/*
 * DefaultTexturesCatalog.java 5 oct. 2007
 * 
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */
package com.eteks.sweethome3d.io;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import com.eteks.sweethome3d.model.CatalogTexture;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.Library;
import com.eteks.sweethome3d.model.TexturesCatalog;
import com.eteks.sweethome3d.model.TexturesCategory;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.tools.TemporaryURLContent;
import com.eteks.sweethome3d.tools.URLContent;

/**
 * Textures default catalog read from localized resources.
 * @author Emmanuel Puybaret
 */
public class DefaultTexturesCatalog extends TexturesCatalog
{
	/**
	 * The keys of the properties values read in <code>.properties</code> files.
	 */
	public enum PropertyKey
	{
		/**
		 * The key for the ID of a texture (optional). 
		 * Two textures read in a texture catalog can't have the same ID
		 * and the second one will be ignored.   
		 */
		ID("id"),
		/**
		 * The key for the name of a texture (mandatory).
		 */
		NAME("name"),
		/**
		 * The key for the category's name of a texture (mandatory).
		 * A new category with this name will be created if it doesn't exist.
		 */
		CATEGORY("category"),
		/**
		 * The key for the image file of a texture (mandatory). 
		 * This image file can be either the path to an image relative to classpath
		 * or an absolute URL. It should be encoded in application/x-www-form-urlencoded format 
		 * if needed.
		 */
		IMAGE("image"),
		/**
		 * The key for the SHA-1 digest of the image file of a texture (optional). 
		 * This property is used to compare faster catalog resources with the ones of a read home,
		 * and should be encoded in Base64.  
		 */
		IMAGE_DIGEST("imageDigest"),
		/**
		 * The key for the width in centimeters of a texture (mandatory).
		 */
		WIDTH("width"),
		/**
		 * The key for the height in centimeters of a texture (mandatory).
		 */
		HEIGHT("height"),
		/**
		 * The key for the creator of a texture (optional).
		 * By default, creator is <code>null</code>.
		 */
		CREATOR("creator");
		
		private String keyPrefix;
		
		private PropertyKey(String keyPrefix)
		{
			this.keyPrefix = keyPrefix;
		}
		
		/**
		 * Returns the key for the texture property of the given index.
		 */
		public String getKey(int textureIndex)
		{
			return keyPrefix + "#" + textureIndex;
		}
	}
	
	/**
	 * The name of <code>.properties</code> family files in plugin textures catalog files. 
	 */
	public static final String PLUGIN_TEXTURES_CATALOG_FAMILY = "PluginTexturesCatalog";
	
	private static final String ADDITIONAL_TEXTURES_CATALOG_FAMILY = "AdditionalTexturesCatalog";
	
	private List<Library> libraries = new ArrayList<Library>();
	
	/**
	 * Creates a default textures catalog read from resources.
	 */
	public DefaultTexturesCatalog()
	{
		this((File) null);
	}
	
	/**
	 * Creates a default textures catalog read from resources and   
	 * textures plugin folder if <code>texturesPluginFolder</code> isn't <code>null</code>.
	 */
	public DefaultTexturesCatalog(File texturesPluginFolder)
	{
		this(null, texturesPluginFolder);
	}
	
	/**
	 * Creates a default textures catalog read from resources and   
	 * textures plugin folder if <code>texturesPluginFolder</code> isn't <code>null</code>.
	 */
	public DefaultTexturesCatalog(final UserPreferences preferences, File texturesPluginFolder)
	{
		this(preferences, texturesPluginFolder == null ? null : new File[] { texturesPluginFolder });
	}
	
	/**
	 * Creates a default textures catalog read from resources and   
	 * textures plugin folders if <code>texturesPluginFolders</code> isn't <code>null</code>.
	 */
	public DefaultTexturesCatalog(final UserPreferences preferences, File[] texturesPluginFolders)
	{
		List<String> identifiedTextures = new ArrayList<String>();
		
		readDefaultTexturesCatalogs(preferences, identifiedTextures);
		
		if (texturesPluginFolders != null)
		{
			for (File texturesPluginFolder : texturesPluginFolders)
			{
				// Try to load sh3t files from textures plugin folder
				File[] pluginTexturesCatalogFiles = texturesPluginFolder.listFiles(new FileFilter()
				{
					public boolean accept(File pathname)
					{
						return pathname.isFile();
					}
				});
				
				if (pluginTexturesCatalogFiles != null)
				{
					// Treat textures catalog files in reverse order of their version
					Arrays.sort(pluginTexturesCatalogFiles,
							Collections.reverseOrder(OperatingSystem.getFileVersionComparator()));
					for (File pluginTexturesCatalogFile : pluginTexturesCatalogFiles)
					{
						// Try to load the properties file describing textures catalog from current file  
						readPluginTexturesCatalog(pluginTexturesCatalogFile, identifiedTextures);
					}
				}
			}
		}
	}
	
	/**
	 * Creates a default textures catalog read only from resources in the given URLs.
	 */
	public DefaultTexturesCatalog(URL[] pluginTexturesCatalogUrls)
	{
		this(pluginTexturesCatalogUrls, null);
	}
	
	/**
	 * Creates a default textures catalog read only from resources in the given URLs.
	 * Texture image URLs will built from <code>texturesResourcesUrlBase</code> if it isn't <code>null</code>.
	 */
	public DefaultTexturesCatalog(URL[] pluginTexturesCatalogUrls, URL texturesResourcesUrlBase)
	{
		List<String> identifiedTextures = new ArrayList<String>();
		try
		{
			SecurityManager securityManager = System.getSecurityManager();
			if (securityManager != null)
			{
				securityManager.checkCreateClassLoader();
			}
			
			for (URL pluginTexturesCatalogUrl : pluginTexturesCatalogUrls)
			{
				try
				{
					ResourceBundle resource = ResourceBundle.getBundle(PLUGIN_TEXTURES_CATALOG_FAMILY,
							Locale.getDefault(), new URLContentClassLoader(pluginTexturesCatalogUrl));
					this.libraries.add(0, new DefaultLibrary(pluginTexturesCatalogUrl.toExternalForm(),
							UserPreferences.TEXTURES_LIBRARY_TYPE, resource));
					readTextures(resource, pluginTexturesCatalogUrl, texturesResourcesUrlBase, identifiedTextures);
				}
				catch (MissingResourceException ex)
				{
					// Ignore malformed textures catalog
				}
				catch (IllegalArgumentException ex)
				{
					// Ignore malformed textures catalog
				}
			}
		}
		catch (AccessControlException ex)
		{
			// Use only textures accessible through classpath
			ResourceBundle resource = ResourceBundle.getBundle(PLUGIN_TEXTURES_CATALOG_FAMILY, Locale.getDefault());
			readTextures(resource, null, texturesResourcesUrlBase, identifiedTextures);
		}
	}
	
	/**
	 * Returns the furniture libraries at initialization.
	 * @since 4.0 
	 */
	public List<Library> getLibraries()
	{
		return Collections.unmodifiableList(this.libraries);
	}
	
	private static final Map<File, URL> pluginTexturesCatalogUrlUpdates = new HashMap<File, URL>();
	
	/**
	 * Reads plug-in textures catalog from the <code>pluginTexturesCatalogFile</code> file. 
	 */
	private void readPluginTexturesCatalog(File pluginTexturesCatalogFile, List<String> identifiedTextures)
	{
		try
		{
			final URL pluginTexturesCatalogUrl;
			;
			long urlModificationDate = pluginTexturesCatalogFile.lastModified();
			URL urlUpdate = pluginTexturesCatalogUrlUpdates.get(pluginTexturesCatalogFile);
			if (pluginTexturesCatalogFile.canWrite()
					&& (urlUpdate == null || urlUpdate.openConnection().getLastModified() < urlModificationDate))
			{
				// Copy updated resource URL content to a temporary file to ensure textures used in home can safely 
				// reference any file of the catalog file even if its content is changed afterwards
				TemporaryURLContent contentCopy = TemporaryURLContent
						.copyToTemporaryURLContent(new URLContent(pluginTexturesCatalogFile.toURI().toURL()));
				URL temporaryTexturesCatalogUrl = contentCopy.getURL();
				pluginTexturesCatalogUrlUpdates.put(pluginTexturesCatalogFile, temporaryTexturesCatalogUrl);
				pluginTexturesCatalogUrl = temporaryTexturesCatalogUrl;
			}
			else if (urlUpdate != null)
			{
				pluginTexturesCatalogUrl = urlUpdate;
			}
			else
			{
				pluginTexturesCatalogUrl = pluginTexturesCatalogFile.toURI().toURL();
			}
			
			final ClassLoader urlLoader = new URLContentClassLoader(pluginTexturesCatalogUrl);
			ResourceBundle resourceBundle = ResourceBundle.getBundle(PLUGIN_TEXTURES_CATALOG_FAMILY,
					Locale.getDefault(), urlLoader);
			this.libraries.add(0, new DefaultLibrary(pluginTexturesCatalogFile.getCanonicalPath(),
					UserPreferences.TEXTURES_LIBRARY_TYPE, resourceBundle));
			readTextures(resourceBundle, pluginTexturesCatalogUrl, null, identifiedTextures);
		}
		catch (MissingResourceException ex)
		{
			// Ignore malformed textures catalog
		}
		catch (IllegalArgumentException ex)
		{
			// Ignore malformed textures catalog
		}
		catch (IOException ex)
		{
			// Ignore unaccessible catalog
		}
	}
	
	/**
	 * Reads the default textures described in properties files accessible through classpath.
	 */
	private void readDefaultTexturesCatalogs(final UserPreferences preferences, List<String> identifiedTextures)
	{
		// Try to load com.eteks.sweethome3d.io.DefaultTexturesCatalog property file from classpath 
		final String defaultTexturesCatalogFamily = DefaultTexturesCatalog.class.getName();
		readTexturesCatalog(defaultTexturesCatalogFamily, preferences, identifiedTextures);
		
		// Try to load com.eteks.sweethome3d.io.AdditionalTexturesCatalog property file from classpath 
		String classPackage = defaultTexturesCatalogFamily.substring(0, defaultTexturesCatalogFamily.lastIndexOf("."));
		readTexturesCatalog(classPackage + "." + ADDITIONAL_TEXTURES_CATALOG_FAMILY, preferences, identifiedTextures);
	}
	
	/**
	 * Reads textures of a given catalog family from resources.
	 */
	private void readTexturesCatalog(final String texturesCatalogFamily, final UserPreferences preferences,
			List<String> identifiedTextures)
	{
		ResourceBundle resource;
		if (preferences != null)
		{
			// Adapt getLocalizedString to ResourceBundle
			resource = new ResourceBundle()
			{
				@Override
				protected Object handleGetObject(String key)
				{
					try
					{
						return preferences.getLocalizedString(texturesCatalogFamily, key);
					}
					catch (IllegalArgumentException ex)
					{
						throw new MissingResourceException("Unknown key " + key,
								texturesCatalogFamily + "_" + Locale.getDefault(), key);
					}
				}
				
				@Override
				public Enumeration<String> getKeys()
				{
					// Not needed
					throw new UnsupportedOperationException();
				}
			};
		}
		else
		{
			try
			{
				resource = ResourceBundle.getBundle(texturesCatalogFamily);
			}
			catch (MissingResourceException ex)
			{
				return;
			}
		}
		readTextures(resource, null, null, identifiedTextures);
	}
	
	/**
	 * Reads each texture described in <code>resource</code> bundle.
	 * Resources described in texture properties will be loaded from <code>texturesUrl</code> 
	 * if it isn't <code>null</code>. 
	 */
	private void readTextures(ResourceBundle resource, URL texturesCatalogUrl, URL texturesResourcesUrlBase,
			List<String> identifiedTextures)
	{
		CatalogTexture texture;
		for (int i = 1; (texture = readTexture(resource, i, texturesCatalogUrl, texturesResourcesUrlBase)) != null; i++)
		{
			if (texture.getId() != null)
			{
				// Take into account only texture that have an ID
				if (identifiedTextures.contains(texture.getId()))
				{
					continue;
				}
				else
				{
					// Add id to identifiedTextures to be sure that two textures with a same ID
					// won't be added twice to textures catalog (in case they are cited twice
					// in different textures properties files)
					identifiedTextures.add(texture.getId());
				}
			}
			TexturesCategory textureCategory = readTexturesCategory(resource, i);
			add(textureCategory, texture);
		}
	}
	
	/**
	 * Returns the texture at the given <code>index</code> of a 
	 * localized <code>resource</code> bundle. 
	 * @param resource             a resource bundle 
	 * @param index                the index of the read texture
	 * @param texturesCatalogUrl  the URL from which texture resources will be loaded 
	 *            or <code>null</code> if it's read from current classpath.
	 * @param texturesResourcesUrlBase the URL used as a base to build the URL to texture resources  
	 *            or <code>null</code> if it's read from current classpath or <code>texturesCatalogUrl</code>
	 * @return the read texture or <code>null</code> if the piece at the given index doesn't exist.
	 * @throws MissingResourceException if mandatory keys are not defined.
	 */
	/**
	 * Reads each texture described in <code>resource</code> bundle.
	 * Resources described in texture properties will be loaded from <code>texturesUrl</code> 
	 * if it isn't <code>null</code>. 
	 */
	protected CatalogTexture readTexture(ResourceBundle resource, int index, URL texturesUrl,
			URL texturesResourcesUrlBase)
	{
		String name = null;
		try
		{
			name = resource.getString(PropertyKey.NAME.getKey(index));
		}
		catch (MissingResourceException ex)
		{
			// Return null if key name# doesn't exist
			return null;
		}
		Content image = getContent(resource, PropertyKey.IMAGE.getKey(index), PropertyKey.IMAGE_DIGEST.getKey(index),
				texturesUrl, texturesResourcesUrlBase);
		float width = Float.parseFloat(resource.getString(PropertyKey.WIDTH.getKey(index)));
		float height = Float.parseFloat(resource.getString(PropertyKey.HEIGHT.getKey(index)));
		String creator = getOptionalString(resource, PropertyKey.CREATOR.getKey(index));
		String id = getOptionalString(resource, PropertyKey.ID.getKey(index));
		
		return new CatalogTexture(id, name, image, width, height, creator);
	}
	
	/**
	 * Returns the category of a texture at the given <code>index</code> of a 
	 * localized <code>resource</code> bundle. 
	 * @throws MissingResourceException if mandatory keys are not defined.
	 */
	protected TexturesCategory readTexturesCategory(ResourceBundle resource, int index)
	{
		String category = resource.getString(PropertyKey.CATEGORY.getKey(index));
		return new TexturesCategory(category);
	}
	
	/**
	 * Returns a valid content instance from the resource file or URL value of key.
	 * @param resource a resource bundle
	 * @param contentKey the key of a resource file
	 * @param texturesUrl the URL of the file containing the target resource if it's not <code>null</code> 
	 * @param resourceUrlBase the URL used as a base to build the URL to content file  
	 *            or <code>null</code> if it's read from current classpath or <code>texturesUrl</code>.
	 * @throws IllegalArgumentException if the file value doesn't match a valid resource or URL.
	 */
	private Content getContent(ResourceBundle resource, String contentKey, String contentDigestKey, URL texturesUrl,
			URL resourceUrlBase)
	{
		String contentFile = resource.getString(contentKey);
		URLContent content;
		try
		{
			// Try first to interpret contentFile as an absolute URL 
			// or an URL relative to resourceUrlBase if it's not null
			URL url;
			if (resourceUrlBase == null)
			{
				url = new URL(contentFile);
			}
			else
			{
				url = contentFile.startsWith("?") ? new URL(resourceUrlBase + contentFile)
						: new URL(resourceUrlBase, contentFile);
			}
			content = new URLContent(url);
		}
		catch (MalformedURLException ex)
		{
			if (texturesUrl == null)
			{
				// Otherwise find if it's a resource
				content = new ResourceURLContent(DefaultTexturesCatalog.class, contentFile);
			}
			else
			{
				try
				{
					content = new ResourceURLContent(new URL("jar:" + texturesUrl + "!" + contentFile), false);
				}
				catch (MalformedURLException ex2)
				{
					throw new IllegalArgumentException("Invalid URL", ex2);
				}
			}
		}
		String contentDigest = getOptionalString(resource, contentDigestKey);
		if (contentDigest != null && contentDigest.length() > 0)
		{
			try
			{
				ContentDigestManager.getInstance().setContentDigest(content, Base64.decode(contentDigest));
			}
			catch (IOException ex)
			{
				// Ignore wrong digest
			}
		}
		return content;
	}
	
	/**
	 * Returns the value of <code>propertyKey</code> in <code>resource</code>, 
	 * or <code>null</code> if the property doesn't exist.
	 */
	private String getOptionalString(ResourceBundle resource, String propertyKey)
	{
		try
		{
			return resource.getString(propertyKey);
		}
		catch (MissingResourceException ex)
		{
			return null;
		}
	}
}
